package bluknite.util.network;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.logging.Logger;

import bluknite.util.network.exception.HandshakeException;
import bluknite.util.network.exception.NetworkException;
import bluknite.util.network.exception.ServerStartException;

public class TCPServer {
	
	private ServerSocket serverSocket;
	private Collection<TCPConnection> connections;
	
	private TCPConnectionHandlerFactory handlerFactory;
	
	private boolean running = false;
	
	private static Logger logger = Logger.getLogger(TCPManager.class.getName());
	
	/**
	 * Creates and starts a {@code TCPServer} instance at the specified
	 * {@code port}.
	 * @param port
	 * @param handlerFactory abstract handlerFactory for creating {@code TCPConnectionHandler}
	 * instances for each established {@code TCPConnection}. If {@code null},
	 * received objects are ignored.
	 * @throws NetworkException if there is any issue with creating/starting
	 * the {@code TCPServer}.
	 */
	public TCPServer(int port, TCPConnectionHandlerFactory handlerFactory) throws NetworkException {
		connections = new ArrayList<TCPConnection>();
		this.handlerFactory = handlerFactory;
		try {
			serverSocket = new ServerSocket(port);
			running = true;
			(new ConnectionHandler()).start();
			(new CleanupThread()).start();
		} catch (IOException e) {
			logger.severe(e.toString());
			throw new ServerStartException("Error starting TCPServer at port: " + port);
		}
	}
	
	/**
	 * Sends a {@code Serializable} object to all clients currently
	 * connected to this {@code TCPServer}.
	 * @param o the object to send.
	 * @throws NetworkException if there was an error sending to any of the clients.
	 */
	public void broadcast(Serializable o) throws NetworkException {
		synchronized(connections) {
			for(TCPConnection connection : connections) {
				connection.send(o);
			}
		}
	}
	
	/**
	 * Disconnects any {@code TCPConnection}s established with this
	 * {@code TCPServer} instance before shutting down the server.
	 */
	public void stop() {
		running = false;
		synchronized(connections) {
			for(TCPConnection connection : connections) {
				connection.disconnect();
			}
		}
		try {
			serverSocket.close();
		} catch (IOException e) {
			logger.info(e.toString());
		}
	}
	
	/**
	 * Private inner {@code Thread} class that handles accepting
	 * inbound connections and spawns instances of {@code HandshakeHandler}
	 * to handle accepted connections.
	 * @author Siraj Ahmed
	 */
	private class ConnectionHandler extends Thread {

		@Override
		public void run() {
			while(running) {
				try {
					Socket out = serverSocket.accept();
					HandshakeHandler handler = new HandshakeHandler(out);
					handler.start();
				} catch (IOException e) {
					logger.info(e.toString());
				}
			}
			super.run();
		}
	}
	
	/**
	 * Private inner {@code Thread} class that handles completion
	 * of {@code TCPConnection} for the specified inbound {@code Socket}.
	 */
	private class HandshakeHandler extends Thread {
		
		private Socket out;
		
		public HandshakeHandler(Socket out) {
			this.out = out;
		}

		@Override
		public void run() {
			if(running) {
				try {
					logger.info("Received outbound connection.");
					String client_host = out.getInetAddress().getHostAddress();
					logger.info("Connection received from client at: " + client_host);
					logger.info("Opening socket to receive second connection...");
					ServerSocket secondServer = new ServerSocket(0);
					int secondPort = secondServer.getLocalPort();
					logger.info("Creating ObjectOutputStream to client...");
					ObjectOutputStream os = new ObjectOutputStream(out.getOutputStream());
					logger.info("Sending second port to client...");
					os.writeInt(secondPort);
					os.flush();
					logger.info("Waiting for client to connect to second port...");
					Socket in = secondServer.accept();
					TCPConnectionHandler handler = null;
					if(handlerFactory != null) {
						handler = handlerFactory.getHandler();
					}
					TCPConnection connection = new TCPConnection(in, out, handler);
					synchronized(connections) {
						connections.add(connection);
					}
					logger.info("Connection with client complete.");
				} catch (IOException e) {
					logger.severe(e.toString());
					NetworkException he = new HandshakeException("Error establishing handshake");
					logger.severe(he.toString());
				} catch (NetworkException e) {
					logger.severe(e.toString());
				}
			}
			super.run();
		}
	}
	
	/**
	 * Private inner {@code Thread} class that takes care
	 * of cleaning up the connections list.
	 */
	private class CleanupThread extends Thread {

		@Override
		public void run() {
			while(running) {
				synchronized(connections) {
					Iterator<TCPConnection> connIter = connections.iterator();
					while(connIter.hasNext()) {
						TCPConnection connection = connIter.next();
						if(!connection.isConnected()) {
							connIter.remove();
						}
					}
				}
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					logger.severe(e.toString());
				}
			}
			super.run();
		}
	}
}